数据结构与算法-字符串与字符串匹配算法
先说说最基础的字符串的数组存储表示:
C语言中顺序串的存储分配可分为两种:
(1)静态分配的数组表示:
#define maxSize256 typedef char SeqString[maxSize];
长度定义为256,实际只能存储255个字符(最后用“\0”表示串值终结)
如需要记录字符串当前实际字符个数,则:
#define maxSize256 typedef struct{ char ch[maxSize]; int curLength; } SeqString;
这种字符串表示简单,但存储空间是在程序编译时静态分配的,放满再存会产生溢出。数组空间不能扩展,程序中止。
(2)动态分配的数组表示:
可以使用new、delete等动态存储管理的函数,根据实际需要动态的分配和释放字符串的存储空间。
这样定义的顺序串类型也有两种形式:
typedef char *SeqString;
或者
#define MaxSize256 typedef struct{ char *ch; int curLength; } SeqString;
ch = new char[maxSize]; if(ch==NULL) exit(1);
这种存储方式处理简单,但预先定义了数组大小,不能适应扩展空间需要
#define maxSize256 typedef struct{ char *ch; int maxSize; int curLength; } SeqString;
在初始化时进行动态存储分配,对结构定义中所有数据成员赋值
void initString(SeqString s){ s.ch = new char[defaultSize]; if(s.ch==NULL) exit(1); s.ch[0] = ‘\0’; s.maxSize = defaultSize; s.curLength = 0 }
当数组空间放满,对其进行成倍扩充
void overflowProcess(){ char *newAddress = newchar[2*maxSize]; if(newAddress==NULL) {cerr<<“Memory Allocation Error”<<endl;exit(1);} intn = maxSize = 2*maxSize; char *srcptr = ch; char *desptr = newAddress; while(n—) *desptr++ = *srcptr++; delete []ch; ch = newAddress; }
string与string.h的区别:
标准C中是不存在string类型的,string是标准C++扩充字符串操作的一个类。而C++的string类操作对象是string类型字符串,该类重装了一些运算符,添加了一些字符串操作成员函数,使得操作字符串更加方便。有的时候我们要将string串和char*串配合使用,所以也会涉及到这两个类型的转化问题。
标准C中有string.h这个头文件,string.h这个头文件中定义了一些我们经常用到的操作字符串的函数,如:strcpy、strcat、strcmp等等,但是这些函数的操作对象都是char*指向的字符串。
<string.h>常用方法:
对于char型指针,在声明时可以直接赋值
char ch[] = "abc";
我试了一下,这么赋值即使没指定大小,但字符串的大小实际还是已经决定好了。
注意:对于char数组型变量,在非声明的时候不能直接赋值,只能通过ch[i]慢慢修改了
- 字符串复制
char* strcpy(char *string1 , char *string2)
把字符串string2的内容复制给string1,如果string1有内容则会被覆盖。
如果string1长度更大,则只覆盖前面部分。
如果string2长度更大,则string1会变成和string2一样。
char* strncpy(char *string1 , char *string2 , int n)
只复制前n个字符的部分复制,会把前n个字符覆盖掉。
char* strdup(char *string1)
为字符串string1分配内存空间,返回值为指向该内存开始地址点指针,
即拷贝字符串string1的一个副本,由函数返回值返回。这个副本有自己的内存空间,和string1不相干
- 字符串连接
char* strcat(char *string1 , char *string2)
连接字符串string2到string1后面。string2原内容保持不变。
char* strncat(char *string1 , char *string2 , int n)
将特定数量字符连接到另一字符串后面
- 在给定字符串中搜索指定字符
char* strchr(char *string1 , char ch)
试了一下,第二个参数可以是’a’,也可以是char ch
返回指向字符ch的首指针。若搜索失败则返回NULL
char* strrchr(char *string1 , char ch)
在给定字符串中搜寻某个指定字符最后一次出现的地址
unsigned long strcspn(char *string1 , char *string2)
在给定字符串中搜寻某一个指定字符第一次出现的位置,从0开始计数。(即这个字符串前面还有多少个字符)
注意:第二个参数必须是char型指针
char* strpbrk(const char *string1 , const char *string2)
在两个字符串中寻找首次出现的共同字符,返回该字符在string1中的地址
char* strstr(const char *string1 , const char *string2)
在第一个字符串周搜索第二个字符串,返回它在第一个字符串中的地址。
- 计算字符串的长度strlen
unsigned long strlen(cons tchar *string1)
- 字符串比较大小
int strcmp(char *string1 , char *string2)
返回结果为正时,说明第一个字符串>第二个字符串
<string>常用方法:
int main(){ string str; str = "Hello world"; // 给str赋值为"Hello world" char cstr[] = "abcde"; //定义了一个C字符串 string s1(str); string s2(str,6); //将str内,开始于位置6的部分当作s2的初值(从0开始) string s3(str,6,3); //将str内,开始于6且长度最多为3的部分作为s3的初值 string s4(cstr); //将C字符串作为s4的初值 string s5(cstr,3); //将C字符串前3个字符作为字符串s5的初值。 string s6(5,'A'); //生成一个字符串,包含5个'A'字符 string s7(str.begin(),str.begin()+5); //区间str.begin()~str.begin()+5内的5个字符作为初值 return 0; }
此外可以直接对string对象进行赋值
str = “abc”; str = str + “abc”; str += “abc”;
可用下列函数来获得string的一些特性:
int capacity()const; //返回当前容量(即string中不必增加内存即可存放的元素个数) int max_size()const; //返回string对象中可存放的最大字符串的长度 int size()const; //返回当前字符串的大小 int length()const; //返回当前字符串的长度 bool empty()const; //当前字符串是否为空 void resize(int len , char c); //把字符串当前大小置为len,多去少补。第二个参数c可以省略,表示补的时候拿c补充
注意:length()函数返回字符串的长度,这个数字应该和size()返回的数字相同
size_type find(const basic_string &str , size_type index); //返回str在字符串中第一次出现的位置(从index开始查找) size_type find(const char *str , size_type index); size_type find(const char *str , size_type index , size_type length); //搜索长度为length size_type find(char ch , size_type index); // 返回字符ch在字符串中第一次出现的位置(从index开始查找)
这些string类的查找函数,都有唯一的返回类型size_type,即一个无符号整数。
若查找成功,返回按查找规则找到的第一个字符或子串的位置(从0开始,相当于该字符前面有多少个字符)
若查找失败,返回npos,npos定义如下:
static const size_type npos = -1;
因此查找字符串A是否包含子串B,不是用 strA.find(strB) > 0 而是 strA.find(strB) != string:npos 。
rfind()与find()很相似,差别在于查找顺序不一样,rfind()是从指定位置起向前查找,直到串首(index还是从前往后数的)
find_first_of( )在源串中从位置pos起往后查找,只要在源串中遇到一个字符,该字符与目标串中任意一个字符相同,就停止查找
find_first_not_of( )在源串中从位置pos开始往后查找,直到遇到某个字符与目标串中的任意一个字符都不相同,才停止查找
find_last_of( )和find_last_not_of( )从指定位置起向前查找
此外,还有一些常用函数:
string &insert(int p , const string &s); //在str[p]位置插入字符串s string &replace(int p , int n , const char *s); //删除从str[p]开始的n个字符,然后在str[p]处插入串s string &erase(int p , int n); //删除str[p]开始的n个字符,返回修改后的字符串 string substr(int pos = 0 , int n = npos)const; //返回pos开始的n个字符组成的字符串 void swap(string &s2); //交换当前字符串与s2的值 string &append(const char *s); //把字符串s连接到当前字符串结尾 void push_back(char c) //当前字符串尾部加一个字符c const char *data()const; //返回一个非null终止的c字符数组,用于string转const char*。它返回的数组不以空字符终止, const char *c_str()const; //返回一个以null终止的c字符串,用于string转const char*
几种常见的字符串匹配算法:
#include <cstring> #include <cstdio> void search(char *pat,char *txt){ int M=strlen(pat); int N=strlen(txt); for(int i=0; i<N-M; i++){ int j; for(j=0; j<M; j++) if(txt[i+j]!=pat[j]) break; if(j==M) printf("Pattern found at index %d /n",i); } }
外层循环执行N-M+1次,内层循环执行M次,时间复杂度为O((N-M+1)*M)
2.KMP(Knuth-Morris-Pratt)算法
若求到next[q]=k,则前q+1个字符组成的字符串,相同的最长前缀和最长后缀长度为k+1
next[0]一定为-1(只有第一个字符的字符串,不存在相同的最长前缀和最长后缀)
求到next[q]=k后,若第k+2个字符ptr[k+1]和第q+2个字符是一样的,那么next[q+1]=k+1
如果是不一样的,k就变成next[k](next[k]必定是小于k的),直到k最后一直循环到-1为止,再比较若第k+2个字符ptr[k+1]和第q+2个字符是否是一样的
搜索的时候,如果出现str[s_i]和ptr[p_i]不一样,如果这时候p_i还是0那么自然s_i继续往前就行了;如果P_i已经不是0了,s_i就不用往前了,因为重合的那部分字符串前缀后缀有一部分是一样的,直接p_i=next[p_i-1]+1即可
#include <stdio.h> #include <stdlib.h> #include <string.h> void cal_next(char *ptr, int *next, int plen) { next[0]=-1; int k=-1; for (int q=1; q<=plen-1; q++) { while (k>-1 && ptr[k+1]!=ptr[q]) k=next[k]; if (ptr[k+1]==ptr[q]) k=k+1; next[q]=k; } } int KMP(char *str, int slen, char *ptr, int plen, int *next){ //在str中寻找ptr int s_i=0, p_i=0; while (s_i<slen && p_i<plen){ if (str[s_i]==ptr[p_i]){ s_i++; p_i++; } else { if(p_i==0) s_i++; else p_i=next[p_i-1]+1; } } return (p_i==plen)?(s_i-plen):-1; }
看到好多博客上说k = next[k]可以用k--代替,但这样是不对的。
例如acceaccc,求到next[6]=2后,用这种方法会求出next[7]=1,实际上next[7]=-1
因为相同的最长前缀和最长后缀长度为k+1,并不代表这k+1个长度的字符串随便从前从后取一串都是匹配的。只有从前从后取next[k]+1长度的字符串,才是一定前后匹配的。如,最大acceacc长度为3的字符串前后匹配都是acc,并不代表长度为2的字符串ac和cc是前后匹配的。只有next[2]长度的字符串才是一定前后匹配的。
3.BM(Boyer-Moore)算法
int bmMatch(const string & text, const string & pat){ int *bc = getBc(pat); int *gs = getGs(pat); //patAt指向了当前pat和text对齐的位置 int patAt = 0; //cmp指向了当前比较的位置 int cmp; const size_t PATLASTID = pat.length() - 1; const size_t patLen = pat.length(); const size_t textLen = text.length(); while (patAt + patLen <= textLen){ //如果匹配成功,cmp就会来到-1的位置上 //patAt + cmp 指向了text上当前比较的字符 for (cmp = PATLASTID; cmp >= 0 && pat[cmp] == text[patAt + cmp]; --cmp); if (cmp == -1) break; else{ patAt += max(gs[cmp], cmp - bc[text[patAt + cmp]]); } } delete []bc; delete []gs; return (patAt + patLen <= textLen)? patAt : -1; } int *getBc(const string& pattern){ //坏后缀情况下建立bc表 int *bc = new int[256]; //256是字符表的规模大小(ACSII) int len = pattern.length(); for (int i = 0; i < 256; ++i) bc[i] = -1; //坏字符不存在时,为-1 for (int i = 0; i < len; ++i) bc[pattern[i]] = i; return bc; } int *suffixes(const string& pat){ //好后缀情况下构建gc表记录每次需要移动的距离比较困难 const int len = pat.length(); int num; int *suff = new int[len]; //辅助表suffix[i]=x表示以i为边界向左,与模式串后缀匹配的最大长度 suff[len - 1] = len; for (int i = len - 2; i >= 0; —i){ for (num = 0; num <= i && pat[i-num] == pat[len-num-1]; ++num); suff[i] = num; } return suff; } int *getGs(const string& pat){ //构建gc[i]表,记录遇到好后缀时模式串需要移动的距离,i表示好后缀左侧第一个坏字符 const int len = pat.length(); const int lastIndex = len - 1; int *suffix = suffixes(pat); int *gs = new int[len]; for (int i = 0; i < len; ++i) gs[i] = len; //情况一:找不到对应的子串和前缀 //找前缀 for (int i = lastIndex; i >= 0; --i) //情况二:存在我们想要的前缀 if (suffix[i] == i + 1) for (int j = 0; j < lastIndex - i; ++j) if (gs[j] == len) gs[j] = lastIndex - i; for (int i = 0; i < lastIndex; ++i) gs[lastIndex - suffix[i]] = lastIndex - i; //情况一:找中间的匹配子串 delete []suffix; return gs; }